Skip to content

Add credential fill, vaults, TOTP and 1Password tokens, with LLM tools - #142

Merged
AHarmlessPyro merged 23 commits into
mainfrom
credentials
Oct 15, 2026
Merged

AHarmlessPyro merged 23 commits into
mainfrom
credentials

Conversation

@AHarmlessPyro

@AHarmlessPyro AHarmlessPyro commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Adds browser credential fill, vaults and typed secrets to the sync and async clients, plus fill_credential tool definitions for OpenAI, Anthropic and Gemini. It matches browserctrl master at 08c5d5232: #1572 (fill on plain secrets), #1551 (vaults), #1552 (op_token, per-request 1Password fields and 1Password browser fill) and #1573 (totp). It mirrors node-sdk#131.

The credential secret type from #1537 is gone. A login is plain secrets that have allowed origins, usually grouped in a vault (USERNAME, PASSWORD, and optionally OTP from a totp secret). Values are encrypted locally and typed into pages by the platform, so neither the caller's code nor an LLM ever sees them.

API (sync and async; params as dicts or models)

Secrets

  • client.secrets.create({name, value?, description?, type?: "secret" | "totp" | "op_token", allowed_origins?, metadata?})
    • Values are checked locally per type before anything is created.
    • totp: a base32 seed or an otpauth://totp/ URI. The parsing mirrors vm-service's parseTOTPSeed: spaces, hyphens and line breaks are ignored, = padding is allowed only at the end, and lengths 1, 3 or 6 past a multiple of 8 are refused. In a URI, algorithm, digits and period are range-checked, and HOTP is refused.
    • op_token: a 1Password service account token, named by one_password.token. It takes no allowed origins.
  • add_version(secret, value) checks the value against the secret's stored type.
  • set_allowed_origins(secret, origins | None), update(secret, {description?, metadata?}) and list({type}).
  • Secret.type is a plain string.
  • create_credential and object values are removed.

Vaults (new client.vaults, by ID or exact name)

  • list, get, delete.
  • create({name, description?, entries?}) and update(vault, {description?, entries?: {ENV: StoredEntry | None}}), where None removes an entry. Vaults hold stored secrets only, as on master; the params are strict, so a leftover source or {item, field} entry is refused rather than dropped.

Sandboxes

  • vault on create (image launches), exec and processes.start. secrets wins on a clash.
  • one_password=OnePasswordSource(token, vault, fields={ENV: OnePasswordField(item, section, field)}) (or a dict) on the same calls reads fields of a 1Password vault when the process starts; it's sent as onePassword.
  • Bindings have optional name, secret_id and version, plus secret_type, reference, token_secret, source (vault, secrets or onePassword) and overrides.
  • The handle keeps startup_secrets across refreshes.

Sessions

  • credentials: [str | {"secret": ...} | {"vault": ...} | {"one_password": ...}] on create. {"one_password": "op-token"} (sent as onePassword) attaches an op_token so fills can read 1Password fields.
  • client.sessions.credentials.fill(session_id, {credential? | vault?, fields: [{selector, field?}], page_id? | page_url?, frame_url?})
    • It returns version, page_id, origin and fields[selector, outcome, secret_id, version, field, expires_at].
    • The request is checked locally, and the timeout is at least 30 s.
  • 1Password fill: fill(session_id, {"one_password": {"token", "vault", "allowed_origins"}, "fields": [{"selector", "ref": {"item", "section", "field"}}]}). Field results gain reference, and there's no top-level version. Checked locally like node.
  • The event log types gain credential_filled and credential_fill_refused.

Tools: CredentialFillTool / fill_credential (OpenAI strict, Anthropic, Gemini)

  • It has the same inputs as node: credential, vault, fields[{selector, field}], page_id, page_url and frame_url.
  • None and "" count as absent, and input that isn't an object returns invalid_request JSON.
  • It doesn't offer 1Password fills: those name their allowed origins in the request, and a model shouldn't choose them. Apps call fill directly.
  • Results and errors never contain values.

README: the secrets, vaults and credential fill section is rewritten. It covers onePassword, error codes and the tool loops.

Validation split: the SDK checks request shapes, secret values (UTF-8, size, no NUL) and TOTP seeds locally, since the API can't see an encrypted value. Policy rules (allowed-origin syntax, 1Password names and the 512-byte reference, field and entry limits, source clashes) are left to the API, which returns its status, code, retryable flag and details unchanged. See docs/credentials-cleanup.md.

Version

The package is bumped to 1.13.1 (published: 1.12.1).

Validation

  • Local checks: after the rebase onto main and the validation cleanup, 1004 tests pass on Python 3.12 (including the mypy and Pyright typing tests) and 1000 on 3.8. Ruff is clean.
  • Audit against master 08c5d5232 (before the validation cleanup): every request, response, error and README claim was checked against the merged server. Fixes:
    • variable names are limited to 256 characters;
    • README: the frame_url tab rule, SECRET_ONEPASSWORD_AUTH_FAILED for malformed tokens, and the 503 variants.
  • End to end (before the validation cleanup): 33/33 checks passed against a local stack running #1552 and #1573:
    • secrets with origins;
    • TOTP create, with bad seeds refused locally;
    • a vault with USERNAME/PASSWORD/OTP, and a session with [{"vault"}];
    • vault and single-secret fills; the typed TOTP code matches a locally computed one;
    • the ambiguous-tab retry and origin refusal;
    • a real login, the event log, and async calls.
  • Model loop: one gpt-5-mini loop filled by vault, recovered from CREDENTIAL_PAGE_AMBIGUOUS with page_id, and logged in.
  • TOTP parsing: compared with #1573's parseTOTPSeed (including its line-break fix, 456995310) on 143 edge cases, alongside node-sdk, hx and the dashboard: no differences.
  • 1Password, live (run before the validation cleanup; not repeated since): 25/25 checks passed against the same stack with a real 1Password service account:
    • one_password on sandbox create (the startup command, read through startup_secrets), exec and processes.start; the values matched by hash;
    • the bindings and the clash, missing-item and wrong-type errors;
    • 1Password fill with origin, not-attached and missing-item refusals, then a real login on scrapingcourse.com;
    • no response contains the token.

Needs browserctrl #1552 and #1573 (both merged) to be deployed for op_token, one_password and totp.

🤖 Generated with Claude Code

https://claude.ai/code/session_019dbwbLvQGyzwkxNetvqf3v

@AHarmlessPyro
AHarmlessPyro marked this pull request as ready for review October 14, 2026 00:46
@AHarmlessPyro AHarmlessPyro changed the title Add website credentials, credential fill and LLM tools Add credential fill, vaults, TOTP and 1Password tokens, with LLM tools Oct 14, 2026
AHarmlessPyro and others added 21 commits October 14, 2026 18:28
Credentials are secrets of type "credential" whose value maps field names
to strings. The value is checked locally, serialized as compact UTF-8 JSON
and sealed with the encryption context the API returns, like any secret.

- Secret gains type and metadata (browser.allowedOrigins); create accepts
  type, metadata and a mapping value; list filters by type; update sends
  metadata with null kept, so a member can be cleared.
- secrets.create_credential() and secrets.set_allowed_origins().
- add_version() checks the value's shape against the secret's type.
- sessions.create() takes credentials; the session lists attached ones.
- sessions.credentials.fill() validates the request locally and posts it
  with a timeout of at least 30 seconds.
- A str secret value that isn't valid UTF-8 (a lone surrogate) now raises
  invalid_secret_value instead of UnicodeEncodeError.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CredentialFillTool exposes sessions.credentials.fill() to a model with
the session ID bound by the application. The model picks the credential
by name, the fields and their selectors, and optionally a page ID or an
iframe URL; it never sees values. runnable() and async_runnable() return
the fill result as compact JSON, or the error's code, message,
retryable flag and server details as JSON so the model can react, for
example by choosing a page after CREDENTIAL_PAGE_AMBIGUOUS.

The definitions come for OpenAI (strict), Anthropic and Gemini. Gemini
schemas are an OpenAPI subset, so tools/gemini.py converts the JSON
schemas (nullable instead of a null type, no additionalProperties), and
every existing tool gains a gemini_tool_definition too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Secret.type is now a str defaulting to "secret", so a new type doesn't
break list() or get(). Requests still accept only the known SecretType
values. The README's LLM samples use gpt-5, claude-sonnet-5-5 and
gemini-3.5-flash.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FzcEMZLVjoiueoQFxG6F7S
… the README

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FzcEMZLVjoiueoQFxG6F7S
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FzcEMZLVjoiueoQFxG6F7S
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019dbwbLvQGyzwkxNetvqf3v
The credential secret type is gone: a secret is fillable once it has
allowed origins, and a login is usually several secrets grouped in a
vault. Secrets take type totp (checked locally as the platform reads
seeds) or op_token, and allowed_origins on create. client.vaults manages
stored and 1Password vaults; sandboxes and processes take vault next to
secrets, and report each binding's source, type and 1Password reference.
Sessions attach secrets or vaults, fills take credential or vault with
page_url, and fill_credential lets a model choose either.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019dbwbLvQGyzwkxNetvqf3v
Strip one leading BOM, then Go's whitespace; uppercase per character so
ß and ff don't become base32; refuse stray % outside the query, trailing
newlines in digits or period, and malformed userinfo; accept userinfo
before totp as Go does; read query pairs as Go's ParseQuery. The fill
tool answers input that isn't an object with an invalid_request error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019dbwbLvQGyzwkxNetvqf3v
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019dbwbLvQGyzwkxNetvqf3v
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019dbwbLvQGyzwkxNetvqf3v
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019dbwbLvQGyzwkxNetvqf3v
browserctrl #1552 no longer links vaults to 1Password: vaults hold stored
secrets only, and 1Password fields are named per request with onePassword
on sandbox create and process start.

- Vaults: drop source and the 1Password entry, source and token models.
  Entries are StoredEntry(name, version), reported as VaultEntry, and None
  in an update removes one. Create and update params refuse unknown members
  such as a leftover source; a source in a response is ignored.
- Sandboxes: one_password=OnePasswordSource(token, vault, fields) on create
  (image launches), exec and start, sync and async, sent as onePassword.
  Token and env names, part rules (UTF-16 length, no '/', '?', Cc, Cf, Zl
  or Zp, not '.' or '..'), field queries, the 512-byte reference, 1-32
  fields, strict members, and clashes with secrets or env are all checked
  before any request. A clash with the vault is left to the server.
- Bindings document source type "onePassword" with reference and
  token_secret.
- README: one_password examples and rules, stored-only vaults, and the
  error codes without VAULT_ENTRY_INVALID and VAULT_SOURCE_INVALID.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019dbwbLvQGyzwkxNetvqf3v
Sessions attach an op_token with credentials=[{"one_password": name}],
sent as {"onePassword": name}. sessions.credentials.fill() takes
one_password={"token", "vault", "allowed_origins"} with a ref
{"item", "section"?, "field"} on every field, sent as onePassword /
allowedOrigins, and checks it all locally as #1552 does. Results gain
each field's op:// reference; the top-level version stays optional.

Allowed origins are now checked locally as the server normalizes them,
for secrets (create, update, set_allowed_origins) and for 1Password
fills alike. fill_credential stays without 1Password: a model must not
choose the origins its values may be typed on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019dbwbLvQGyzwkxNetvqf3v
#1573's vm-service drops CR and LF from a seed with its spaces and hyphens
before the length check (its base32 decoder skips them), so a seed pasted
across lines is valid. The clients refused it; they now drop them too. A raw
line break inside an otpauth URI is still refused, as Go's url.Parse does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019dbwbLvQGyzwkxNetvqf3v
Allowed origins are trimmed the way JavaScript's trim() does, so a
leading byte-order mark is accepted and \x1c-\x1f or \x85 are not, and an
IPv6 zone is checked as Node's isIPv6 does, which Python 3.8 refused
outright. A `secrets` variable name longer than 256 characters is now
refused locally, as the server refuses it.

The README now says a frame fill skips the allowed-origin tab step, that
SECRET_ONEPASSWORD_AUTH_FAILED also covers a malformed token or one for
another sign-in host, and the 503 that CREDENTIAL_FILL_FAILED and
SECRET_ONEPASSWORD_FAILED can carry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019dbwbLvQGyzwkxNetvqf3v
The API has sent tab_opened events since browserctrl #559, and the event
type Literal refused them, so listing such a session's events failed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019dbwbLvQGyzwkxNetvqf3v
@AHarmlessPyro
AHarmlessPyro added this pull request to the merge queue Oct 15, 2026
@AHarmlessPyro
AHarmlessPyro removed this pull request from the merge queue due to a manual request Oct 15, 2026
@AHarmlessPyro
AHarmlessPyro merged commit 60e1d94 into main Oct 15, 2026
13 checks passed
@AHarmlessPyro
AHarmlessPyro deleted the credentials branch October 15, 2026 18:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants